反向传播如何实现?数学原理?

通俗解释

在神经网络的训练过程中,我们使用反向传播(backpropagation)算法来计算权重矩阵 ( W ) 的梯度,并用这些梯度来更新 ( W )。这个更新过程是为了使模型的预测误差最小化。反向传播的一个核心部分就是计算每一层的权重矩阵相对于损失函数的导数(梯度)。

计算梯度的原理 - 更新W

前向传播

假设我们有一个线性层,其计算公式是: 其中:

  • ( x ) 是输入数据,形状为 ((\text{batch_size}, \text{num_in}))。
  • ( W ) 是权重矩阵,形状为 ((\text{num_in}, \text{num_out}))。
  • ( y ) 是输出数据,形状为 ((\text{batch_size}, \text{num_out}))。
  • ( b ) 是偏置向量,形状为 ((1, \text{num_out}))。

反向传播

在反向传播中,我们需要计算损失函数 (\mathcal{L}) 对权重矩阵 ( W ) 的导数,即

具体推导

  1. 损失函数的梯度

    首先,我们有损失函数 (\mathcal{L}) 对输出 ( y ) 的梯度

    这个梯度由上一层反向传播给我们,记为 grad,其形状为 ((\text{batch_size}, \text{num_out}))。

  2. 链式法则

    根据链式法则,损失函数对权重矩阵 ( W ) 的梯度可以分解为:

  3. 计算 (\frac{\partial y}{\partial W})

    我们知道:

    所以,输出 ( y ) 对权重矩阵 ( W ) 的导数

    是输入 ( x ):

    具体地,假设 ( x ) 是一个单一的输入向量(形状为 ((\text{num_in}))),则 ( y ) 对 ( W ) 的导数是:

    这意味着 ( y ) 对 ( W ) 的导数是输入 ( x ) 本身。

  4. 矩阵形式的梯度计算

    现在,我们将这些扩展到整个批次。对于每个样本 ( x ) 和对应的输出梯度 grad,我们需要计算:

    其中:

    • ( x ) 的形状为 ((\text{batch_size}, \text{num_in}))
    • grad 的形状为 ((\text{batch_size}, \text{num_out}))
    • ( x^T ) 的形状为 ((\text{num_in}, \text{batch_size}))

    矩阵乘法 ( x^T \cdot \text{grad} ) 的结果是一个形状为 ((\text{num_in}, \text{num_out})) 的矩阵,这正是我们需要的权重矩阵 ( W ) 的梯度。

举个具体例子

假设我们有以下数据:

  • 输入 ( x ):
    形状为 ((3, 3))
  • 反向传播的梯度 grad
    形状为 ((3, 2))

计算 ( x^T \cdot \text{grad} ):

最后,我们对结果除以 batch_size 3 得到平均梯度:

这就是最终的权重矩阵 ( W ) 的梯度。

总结

通过反向传播,我们计算了输入 ( x ) 对权重矩阵 ( W ) 的梯度。这一步的目的是确定如何调整权重 ( W ) 以减少损失。这个过程通过矩阵乘法 ( x^T \cdot \text{grad} ) 实现,并且结果会对 batch_size 进行平均化。这个计算的背后是链式法则,它使得我们能够将梯度从输出层逐层传播到每个参数。

更新b

推导偏置项的梯度更新

在神经网络的训练过程中,我们需要计算每一层的权重矩阵 ( W ) 和偏置向量 ( b ) 对于损失函数 (\mathcal{L}) 的梯度。通过反向传播算法,可以得到这些梯度,并用它们来更新模型参数,以减少损失。

前向传播

假设我们有一个线性层,其计算公式是: 其中:

  • ( x ) 是输入数据,形状为 ((\text{batch_size}, \text{num_in}))。
  • ( W ) 是权重矩阵,形状为 ((\text{num_in}, \text{num_out}))。
  • ( b ) 是偏置向量,形状为 ((1, \text{num_out}))。
  • ( y ) 是输出数据,形状为 ((\text{batch_size}, \text{num_out}))。

反向传播

在反向传播中,我们需要计算损失函数 (\mathcal{L}) 对偏置向量 ( b ) 的导数,即

推导 (\frac{\partial \mathcal{L}}{\partial b})

  1. 损失函数的梯度

    首先,我们有损失函数 (\mathcal{L}) 对输出 ( y ) 的梯度

    这个梯度由上一层反向传播给我们,记为 grad,其形状为 ((\text{batch_size}, \text{num_out}))。

  2. 链式法则

    根据链式法则,损失函数对偏置向量 ( b ) 的梯度可以分解为:

  3. 计算 (\frac{\partial y}{\partial b})

    我们知道:

    偏置向量 ( b ) 对输出 ( y ) 的导数 (\frac{\partial y}{\partial b}) 是 1(因为 ( b ) 是直接加到 ( y ) 上的)。具体地,对于每个输出 ( y_i ),

    由于 ( b ) 对 ( y ) 的每个元素的导数都是 1,我们可以将其看作是对 grad 的所有元素进行累加。因此:

    \frac{\partial \mathcal{L}}{\partial b} = \sum_{i=1}^{\text{batch_size}} \frac{\partial \mathcal{L}}{\partial y_i}

具体实现

在代码中,我们可以用 numpy 来实现这一计算。我们需要对 grad 进行累加,并对 batch_size 取平均值:

def backward(self, grad):
    # 反向传播,按照链式法则计算
    # grad的维度为(batch_size, num_out)
    # 梯度应该对batch_size去平均值
    # grad_W的维度应该与W相同,为(num_in, num_out)
    self.grad_W = self.x.T @ grad / grad.shape[0]
    if self.use_bias:
        # grad_b的维度与b相同,(1, num_out)
        self.grad_b = np.mean(grad, axis=0, keepdims=True)
  • self.grad_W = self.x.T @ grad / grad.shape[0]:计算权重矩阵 WWW 的梯度。我们对输入 xxx 的转置进行矩阵乘法,并除以 batch_size 以计算平均梯度。
  • self.grad_b = np.mean(grad, axis=0, keepdims=True):计算偏置向量 bbb 的梯度。我们对 grad 沿批次维度(第 0 维)取平均值,并保留维度信息以确保结果形状与偏置向量 bbb 一致。

往上一层传递grad

推导传递到上一层的梯度

在反向传播过程中,除了计算当前层的参数梯度(如权重和偏置),我们还需要计算传递到上一层的梯度。这是为了继续将梯度信息传递给网络的更早层,从而更新所有层的参数。下面我们通过公式推导和具体例子来解释这个过程。

前向传播

假设我们有一个线性层,其计算公式是: 其中:

  • ( x ) 是输入数据,形状为 ((\text{batch_size}, \text{num_in}))。
  • ( W ) 是权重矩阵,形状为 ((\text{num_in}, \text{num_out}))。
  • ( y ) 是输出数据,形状为 ((\text{batch_size}, \text{num_out}))。
  • ( b ) 是偏置向量,形状为 ((1, \text{num_out}))。

反向传播

在反向传播中,我们需要计算损失函数 (\mathcal{L}) 对输入 ( x ) 的导数,即 (\frac{\partial \mathcal{L}}{\partial x})。这个导数将作为梯度传递给上一层。

推导 (\frac{\partial \mathcal{L}}{\partial x})

  1. 损失函数的梯度

    首先,我们有损失函数 (\mathcal{L}) 对输出 ( y ) 的梯度 (\frac{\partial \mathcal{L}}{\partial y}),这个梯度由上一层反向传播给我们,记为 grad,其形状为 ((\text{batch_size}, \text{num_out}))。

  2. 链式法则

    根据链式法则,损失函数对输入 ( x ) 的梯度可以分解为:

  3. 计算 (\frac{\partial y}{\partial x})

    我们知道:

    输出 ( y ) 对输入 ( x ) 的导数 (\frac{\partial y}{\partial x}) 就是权重矩阵 ( W ):

    因此,损失函数 (\mathcal{L}) 对输入 ( x ) 的梯度可以表示为:

    在代码中,这可以表示为:

    grad = grad @ self.W.T
    

具体实现

def backward(self, grad):
    # 反向传播,按照链式法则计算
    # grad的维度为(batch_size, num_out)
    # 计算权重和偏置的梯度
    self.grad_W = self.x.T @ grad / grad.shape[0]
    if self.use_bias:
        self.grad_b = np.mean(grad, axis=0, keepdims=True)
    
    # 传递到上一层的梯度
    grad = grad @ self.W.T
    return grad